<script setup>
import useDialogStore from 'stores/dialog.js'
import { h, markRaw, nextTick, reactive, ref } from 'vue'
import useConnectionStore from 'stores/connections.js'
import { NIcon, NSpace, NText, useThemeVars } from 'naive-ui'
import { ConnectionType } from '@/consts/connection_type.js'
import Folder from '@/components/icons/Folder.vue'
import Server from '@/components/icons/Server.vue'
import Cluster from '@/components/icons/Cluster.vue'
import { debounce, get, includes, indexOf, isEmpty, split } from 'lodash'
import Config from '@/components/icons/Config.vue'
import Delete from '@/components/icons/Delete.vue'
import Unlink from '@/components/icons/Unlink.vue'
import CopyLink from '@/components/icons/CopyLink.vue'
import Connect from '@/components/icons/Connect.vue'
import { useI18n } from 'vue-i18n'
import useTabStore from 'stores/tab.js'
import Edit from '@/components/icons/Edit.vue'
import { hexGammaCorrection, parseHexColor, toHexColor } from '@/utils/rgb.js'
import IconButton from '@/components/common/IconButton.vue'
import usePreferencesStore from 'stores/preferences.js'
import useBrowserStore from 'stores/browser.js'
import { useRender } from '@/utils/render.js'

const themeVars = useThemeVars()
const i18n = useI18n()
const render = useRender()
const connectingServer = ref('')
const connectionStore = useConnectionStore()
const browserStore = useBrowserStore()
const tabStore = useTabStore()
const prefStore = usePreferencesStore()
const dialogStore = useDialogStore()

const expandedKeys = ref([])
const selectedKeys = ref([])

const props = defineProps({
  filterPattern: {
    type: String,
  },
})

const contextMenuParam = reactive({
  show: false,
  x: 0,
  y: 0,
  options: null,
  currentNode: null,
})

const menuOptions = {
  [ConnectionType.Group]: ({ opened }) => [
    {
      key: 'group_rename',
      label: 'interface.rename_conn_group',
      icon: Edit,
    },
    {
      key: 'group_delete',
      label: 'interface.remove_conn_group',
      icon: Delete,
    },
  ],
  [ConnectionType.Server]: ({ name }) => {
    const connected = browserStore.isConnected(name)
    if (connected) {
      return [
        {
          key: 'server_close',
          label: 'interface.disconnect',
          icon: Unlink,
        },
        {
          key: 'server_edit',
          label: 'interface.edit_conn',
          icon: Config,
        },
        {
          key: 'server_dup',
          label: 'interface.dup_conn',
          icon: CopyLink,
        },
        {
          type: 'divider',
          key: 'd1',
        },
        {
          key: 'server_remove',
          label: 'interface.remove_conn',
          icon: Delete,
        },
      ]
    } else {
      return [
        {
          key: 'server_open',
          label: 'interface.open_connection',
          icon: Connect,
        },
        {
          key: 'server_edit',
          label: 'interface.edit_conn',
          icon: Config,
        },
        {
          key: 'server_dup',
          label: 'interface.dup_conn',
          icon: CopyLink,
        },
        {
          type: 'divider',
          key: 'd1',
        },
        {
          key: 'server_remove',
          label: 'interface.remove_conn',
          icon: Delete,
        },
      ]
    }
  },
}

/**
 * get mark color of server saved in preferences
 * @param name
 * @return {null|string}
 */
const getServerMarkColor = (name) => {
  const { markColor = '' } = connectionStore.serverProfile[name] || {}
  if (!isEmpty(markColor)) {
    const rgb = parseHexColor(markColor)
    const rgb2 = hexGammaCorrection(rgb, 0.75)
    return toHexColor(rgb2)
  }
  return null
}

const renderLabel = ({ option }) => {
  if (option.type === ConnectionType.Server) {
    const color = getServerMarkColor(option.name)
    if (color != null) {
      return h(
        NText,
        {
          style: {
            color,
            fontWeight: '450',
          },
        },
        () => option.label,
      )
    }
  }
  return option.label
}

// render horizontal item
const renderIconMenu = (items) => {
  return h(
    NSpace,
    {
      align: 'center',
      inline: true,
      size: 3,
      wrapItem: false,
      wrap: false,
      style: 'margin-right: 5px',
    },
    () => items,
  )
}

const renderPrefix = ({ option }) => {
  const iconTransparency = prefStore.isDark ? 0.75 : 1
  switch (option.type) {
    case ConnectionType.Group:
      const opened = indexOf(expandedKeys.value, option.key) !== -1
      return h(
        NIcon,
        { size: 20 },
        {
          default: () =>
            h(Folder, {
              open: opened,
              fillColor: `rgba(255,206,120,${iconTransparency})`,
            }),
        },
      )
    case ConnectionType.Server:
      const connected = browserStore.isConnected(option.name)
      const color = getServerMarkColor(option.name)
      const icon = option.cluster === true ? Cluster : Server
      return h(
        NIcon,
        { size: 20, color: !!!connected ? color : '#dc423c' },
        {
          default: () =>
            h(icon, {
              inverse: !!connected,
              fillColor: `rgba(220,66,60,${iconTransparency})`,
            }),
        },
      )
  }
}

const getServerMenu = (connected) => {
  const btns = []
  if (connected) {
    btns.push(
      h(IconButton, {
        tTooltip: 'interface.disconnect',
        icon: Unlink,
        onClick: () => handleSelectContextMenu('server_close'),
      }),
      h(IconButton, {
        tTooltip: 'interface.edit_conn',
        icon: Config,
        onClick: () => handleSelectContextMenu('server_edit'),
      }),
    )
  } else {
    btns.push(
      h(IconButton, {
        tTooltip: 'interface.open_connection',
        icon: Connect,
        onClick: () => handleSelectContextMenu('server_open'),
      }),
      h(IconButton, {
        tTooltip: 'interface.edit_conn',
        icon: Config,
        onClick: () => handleSelectContextMenu('server_edit'),
      }),
      h(IconButton, {
        tTooltip: 'interface.remove_conn',
        icon: Delete,
        onClick: () => handleSelectContextMenu('server_remove'),
      }),
    )
  }
  return btns
}

const getGroupMenu = () => {
  return [
    h(IconButton, {
      tTooltip: 'interface.rename_conn_group',
      icon: Config,
      onClick: () => handleSelectContextMenu('group_rename'),
    }),
    h(IconButton, {
      tTooltip: 'interface.remove_conn_group',
      icon: Delete,
      onClick: () => handleSelectContextMenu('group_delete'),
    }),
  ]
}

const renderSuffix = ({ option }) => {
  if (includes(selectedKeys.value, option.key)) {
    switch (option.type) {
      case ConnectionType.Server:
        const connected = browserStore.isConnected(option.name)
        return renderIconMenu(getServerMenu(connected))
      case ConnectionType.Group:
        return renderIconMenu(getGroupMenu())
    }
  }
  return null
}

const onUpdateExpandedKeys = (keys, option) => {
  expandedKeys.value = keys
}

const onUpdateSelectedKeys = (keys, option) => {
  selectedKeys.value = keys
}

/**
 * Open connection
 * @param name
 * @returns {Promise<void>}
 */
const openConnection = async (name) => {
  try {
    connectingServer.value = name
    if (!browserStore.isConnected(name)) {
      await browserStore.openConnection(name)
    }
    // check if connection already canceled before finish open
    if (!isEmpty(connectingServer.value)) {
      tabStore.upsertTab({
        server: name,
        db: browserStore.getSelectedDB(name),
      })
    }
  } catch (e) {
    $message.error(e.message)
    // node.isLeaf = undefined
  } finally {
    connectingServer.value = ''
  }
}

const removeConnection = (name) => {
  $dialog.warning(i18n.t('dialogue.remove_tip', { type: i18n.t('dialogue.connection.conn_name'), name }), async () => {
    connectionStore.deleteConnection(name).then(({ success, msg }) => {
      if (!success) {
        $message.error(msg)
      }
    })
  })
}

const removeGroup = async (name) => {
  $dialog.warning(i18n.t('dialogue.remove_group_tip', { name }), async () => {
    connectionStore.deleteGroup(name).then(({ success, msg }) => {
      if (!success) {
        $message.error(msg)
      }
    })
  })
}

const expandKey = (key) => {
  const idx = indexOf(expandedKeys.value, key)
  if (idx === -1) {
    expandedKeys.value.push(key)
  } else {
    expandedKeys.value.splice(idx, 1)
  }
}

const nodeProps = ({ option }) => {
  return {
    onDblclick: async () => {
      if (option.type === ConnectionType.Server) {
        openConnection(option.name).then(() => {})
      } else if (option.type === ConnectionType.Group) {
        // toggle expand
        nextTick().then(() => expandKey(option.key))
      }
    },
    onContextmenu(e) {
      e.preventDefault()
      const mop = menuOptions[option.type]
      if (mop == null) {
        return
      }
      contextMenuParam.show = false
      nextTick().then(() => {
        contextMenuParam.options = markRaw(mop(option))
        contextMenuParam.currentNode = option
        contextMenuParam.x = e.clientX
        contextMenuParam.y = e.clientY
        contextMenuParam.show = true
        selectedKeys.value = [option.key]
      })
    },
  }
}

const handleSelectContextMenu = (key) => {
  contextMenuParam.show = false
  const selectedKey = get(selectedKeys.value, 0)
  if (selectedKey == null) {
    return
  }
  const [group, name] = split(selectedKey, '/')
  if (isEmpty(group) && isEmpty(name)) {
    return
  }
  switch (key) {
    case 'server_open':
      openConnection(name).then(() => {})
      break
    case 'server_edit':
      // ask for close relevant connections before edit
      if (browserStore.isConnected(name)) {
        $dialog.warning(i18n.t('dialogue.edit_close_confirm'), () => {
          browserStore.closeConnection(name)
          dialogStore.openEditDialog(name)
        })
      } else {
        dialogStore.openEditDialog(name)
      }
      break
    case 'server_dup':
      dialogStore.openDuplicateDialog(name)
      break
    case 'server_remove':
      removeConnection(name)
      break
    case 'server_close':
      browserStore.closeConnection(name).then((closed) => {
        if (closed) {
          $message.success(i18n.t('dialogue.handle_succ'))
        }
      })
      break
    case 'group_rename':
      if (!isEmpty(group)) {
        dialogStore.openRenameGroupDialog(group)
      }
      break
    case 'group_delete':
      if (!isEmpty(group)) {
        removeGroup(group)
      }
      break
    default:
      console.warn('TODO: handle context menu:' + key)
  }
}

const findSiblingsAndIndex = (node, nodes) => {
  if (!nodes) {
    return [null, null]
  }
  for (let i = 0; i < nodes.length; ++i) {
    const siblingNode = nodes[i]
    if (siblingNode.key === node.key) {
      return [nodes, i]
    }
    const [siblings, index] = findSiblingsAndIndex(node, siblingNode.children)
    if (siblings && index !== null) {
      return [siblings, index]
    }
  }
  return [null, null]
}

// delay save until drop stopped after 2 seconds
const saveSort = debounce(connectionStore.saveConnectionSorted, 2000, { trailing: true })
const handleDrop = ({ node, dragNode, dropPosition }) => {
  const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections)
  if (dragNodeSiblings === null || dragNodeIndex === null) {
    return
  }
  if (node.type === ConnectionType.Group && dragNode.type === ConnectionType.Group) {
    return
  }
  dragNodeSiblings.splice(dragNodeIndex, 1)
  if (dropPosition === 'inside') {
    if (node.children) {
      node.children.unshift(dragNode)
    } else {
      node.children = [dragNode]
    }
  } else if (dropPosition === 'before') {
    const [nodeSiblings, nodeIndex] = findSiblingsAndIndex(node, connectionStore.connections)
    if (nodeSiblings === null || nodeIndex === null) {
      return
    }
    nodeSiblings.splice(nodeIndex, 0, dragNode)
  } else if (dropPosition === 'after') {
    const [nodeSiblings, nodeIndex] = findSiblingsAndIndex(node, connectionStore.connections)
    if (nodeSiblings === null || nodeIndex === null) {
      return
    }
    nodeSiblings.splice(nodeIndex + 1, 0, dragNode)
  }
  connectionStore.connections = Array.from(connectionStore.connections)
  saveSort()
}

const onCancelOpen = () => {
  if (!isEmpty(connectingServer.value)) {
    browserStore.closeConnection(connectingServer.value)
    connectingServer.value = ''
  }
}
</script>

<template>
  <div class="connection-tree-wrapper" @keydown.esc="contextMenuParam.show = false">
    <n-tree
      :animated="false"
      :block-line="true"
      :block-node="true"
      :cancelable="false"
      :data="connectionStore.connections"
      :draggable="true"
      :expanded-keys="expandedKeys"
      :node-props="nodeProps"
      :pattern="props.filterPattern"
      :render-label="renderLabel"
      :render-prefix="renderPrefix"
      :render-suffix="renderSuffix"
      :selected-keys="selectedKeys"
      class="fill-height"
      virtual-scroll
      @drop="handleDrop"
      @update:selected-keys="onUpdateSelectedKeys"
      @update:expanded-keys="onUpdateExpandedKeys">
      <template #empty>
        <n-empty :description="$t('interface.empty_server_list')" class="empty-content" />
      </template>
    </n-tree>

    <!-- status display modal -->
    <n-modal :show="connectingServer !== ''" transform-origin="center">
      <n-card
        :bordered="false"
        :content-style="{ textAlign: 'center' }"
        aria-model="true"
        role="dialog"
        style="width: 400px">
        <n-spin>
          <template #description>
            <n-space vertical>
              <n-text strong>{{ $t('dialogue.opening_connection') }}</n-text>
              <n-button :focusable="false" secondary size="small" @click="onCancelOpen">
                {{ $t('dialogue.interrupt_connection') }}
              </n-button>
            </n-space>
          </template>
        </n-spin>
      </n-card>
    </n-modal>

    <!-- context menu -->
    <n-dropdown
      :keyboard="true"
      :options="contextMenuParam.options"
      :render-icon="({ icon }) => render.renderIcon(icon)"
      :render-label="({ label }) => render.renderLabel($t(label), { class: 'context-menu-item' })"
      :show="contextMenuParam.show"
      :x="contextMenuParam.x"
      :y="contextMenuParam.y"
      placement="bottom-start"
      trigger="manual"
      @clickoutside="contextMenuParam.show = false"
      @select="handleSelectContextMenu" />
  </div>
</template>

<style lang="scss" scoped>
@import '@/styles/content';

.connection-tree-wrapper {
  height: 100%;
  overflow: hidden;
}
</style>
